#!/usr/bin/env python3
"""
Dify知识库管理API服务
提供RESTful接口供其他工程师调用
"""

from flask import Flask, request, jsonify, g
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from werkzeug.utils import secure_filename
from pathlib import Path
import os
import tempfile
import requests
import io
import secrets
import ipaddress
from typing import Dict, Any, List
from loguru import logger
from dify_client import DifyAPIClient
from config import settings
from config import simple_id_to_uuid, get_app_key_by_number  # 新增导入简单ID映射和app key获取函数
from workflow_client import workflow_client
from functools import wraps

app = Flask(__name__)
CORS(app, origins=settings.allowed_origins.split(',') if settings.allowed_origins != '*' else '*')

# 配置限流器
limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=[f"{settings.rate_limit_per_minute} per minute", f"{settings.rate_limit_per_hour} per hour"] if settings.rate_limit_enabled else []
)

# 配置上传文件临时目录
UPLOAD_FOLDER = tempfile.mkdtemp()
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB 最大文件大小

# 统一响应格式
def create_response(code: int = 200, message: str = "", data: Any = None, error: str = None):
    """创建统一格式的响应"""
    response = {
        "code": code,
        "message": message,
        "data": data,
        "error": error
    }
    return jsonify(response), code

# 权限验证函数
def check_dataset_permission(dataset_id: str) -> bool:
    """检查知识库是否在允许操作的列表中"""
    # 如果是简单ID，转换为实际UUID
    if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
        dataset_id = simple_id_to_uuid[dataset_id]
    
    managed_knowledge_bases = settings.get_all_managed_knowledge_bases()
    
    # 如果没有配置知识库，允许操作所有知识库（向后兼容）
    if not managed_knowledge_bases:
        return True
    
    # 检查知识库是否在允许操作的列表中
    return dataset_id in managed_knowledge_bases

def require_dataset_permission(dataset_id: str):
    """装饰器：要求知识库权限"""
    if not check_dataset_permission(dataset_id):
        return create_response(403, "无权操作此知识库", error=f"知识库 {dataset_id} 不在允许操作的列表中")
    return None

def get_knowledge_base_info(dataset_id: str) -> Dict[str, Any]:
    """获取知识库信息，包含工作流key"""
    kb_config = settings.get_knowledge_base_config(dataset_id)
    if kb_config:
        return {
            "workflow_key": kb_config.get("workflow_key", ""),
            "workflow_config": kb_config.get("workflow_config", {})
        }
    return {}

def download_file_from_url(url: str) -> tuple[str, bytes]:
    """从URL下载文件
    
    Args:
        url: 文件下载链接
        
    Returns:
        tuple: (文件名, 文件内容字节)
    """
    try:
        response = requests.get(url, timeout=30)
        response.raise_for_status()
        
        # 从URL中提取文件名
        filename = url.split('/')[-1]
        if not filename or '.' not in filename:
            # 如果没有合适的文件名，使用默认名称
            content_type = response.headers.get('content-type', '')
            if 'word' in content_type or 'docx' in content_type:
                filename = 'document.docx'
            elif 'excel' in content_type or 'spreadsheet' in content_type:
                filename = 'document.xlsx'
            elif 'pdf' in content_type:
                filename = 'document.pdf'
            elif 'text' in content_type:
                filename = 'document.txt'
            else:
                filename = 'document.dat'
        
        return filename, response.content
        
    except requests.exceptions.RequestException as e:
        logger.error(f"下载文件失败: {str(e)}")
        raise Exception(f"下载文件失败: {str(e)}")
    except Exception as e:
        logger.error(f"处理下载文件时出错: {str(e)}")
        raise Exception(f"处理下载文件时出错: {str(e)}")

# 错误处理
@app.errorhandler(400)
def bad_request(error):
    return create_response(400, "请求参数错误", error=str(error))

@app.errorhandler(404)
def not_found(error):
    return create_response(404, "接口不存在", error=str(error))

@app.errorhandler(500)
def internal_error(error):
    logger.error(f"服务器内部错误: {str(error)}")
    return create_response(500, "服务器内部错误", error=str(error))

# 健康检查接口
@app.route('/health', methods=['GET'])
def health_check():
    """健康检查"""
    return create_response(200, "服务运行正常", {
        "status": "healthy",
        "service": "dify-kb-api"
    })

# 工作流连接测试接口
@app.route('/api/workflow/test', methods=['GET'])
def test_workflow_connection():
    """测试工作流API连接"""
    try:
        success = workflow_client.test_connection()
        if success:
            return create_response(200, "工作流API连接测试成功", {
                "workflow_enabled": settings.workflow_enabled,
                "workflow_base_url": settings.workflow_base_url,
                "workflow_endpoint": settings.workflow_api_endpoint
            })
        else:
            return create_response(503, "工作流API连接失败", {
                "workflow_enabled": settings.workflow_enabled,
                "workflow_base_url": settings.workflow_base_url,
                "workflow_endpoint": settings.workflow_api_endpoint
            })
    except Exception as e:
        logger.error(f"工作流连接测试失败: {str(e)}")
        return create_response(500, "工作流连接测试失败", error=str(e))

# 知识库管理接口
@app.route('/api/datasets', methods=['GET'])
def list_datasets():
    """获取知识库列表 - 只显示有权限操作的知识库"""
    try:
        client = DifyAPIClient()
        datasets = client.get_datasets()
        
        # 如果配置了知识库，只显示允许操作的知识库
        managed_knowledge_bases = settings.get_all_managed_knowledge_bases()
        if managed_knowledge_bases:
            # 过滤出允许操作的知识库
            filtered_datasets = []
            for dataset in datasets:
                if dataset.get('id') in managed_knowledge_bases:
                    # 添加知识库管理信息（包含工作流key）
                    kb_config = settings.get_knowledge_base_config(dataset.get('id'))
                    if kb_config:
                        dataset['knowledge_base_info'] = {
                            'workflow_key': kb_config.get('workflow_key', ''),
                            'workflow_config': {
                                'enabled': kb_config.get('workflow_config', {}).get('enabled', False),
                                'base_url': kb_config.get('workflow_config', {}).get('base_url', ''),
                                'endpoint': kb_config.get('workflow_config', {}).get('endpoint', '')
                            }
                        }
                    filtered_datasets.append(dataset)
            
            datasets = filtered_datasets
        
        return create_response(200, "获取知识库列表成功", {
            "datasets": datasets,
            "total": len(datasets),
            "managed_only": bool(managed_knowledge_bases)  # 指示是否只显示了受管理的知识库
        })
    except Exception as e:
        logger.error(f"获取知识库列表失败: {str(e)}")
        return create_response(500, "获取知识库列表失败", error=str(e))

@app.route('/api/datasets', methods=['POST'])
def create_dataset():
    """创建知识库"""
    try:
        data = request.get_json(silent=True)
        # 追加：记录原始请求信息，解析实际收到的参数（headers/query/form/files/JSON/body）
        try:
            content_type = request.headers.get('Content-Type', '')
            raw_body = request.get_data(as_text=True)
            preview = raw_body[:500] if raw_body else ''
            args_dict = request.args.to_dict()
            form_items = {k: (v[:200] + '…' if isinstance(v, str) and len(v) > 200 else v) for k, v in request.form.items()}
            try:
                files_info = {k: v.filename for k, v in request.files.items()}
            except Exception:
                files_info = {}
            header_keys = list(request.headers.keys())
            logger.info(
                f"请求信息: method={request.method}, path={request.path}, Content-Type={content_type}, "
                f"args={args_dict}, form_keys={list(form_items.keys())}, files={files_info}, "
                f"body_len={len(raw_body) if raw_body else 0}, body_preview={preview}"
            )
        except Exception:
            pass
        try:
            if data:
                from json import dumps
                keys = list(data.keys())
                serialized = dumps(data, ensure_ascii=False)
                logger.info(f"解析后JSON: keys={keys}, json={serialized[:800]}")
            else:
                logger.warning("解析后JSON为空或解析失败，可能不是application/json或body为空")
        except Exception:
            pass
        if not data:
            return create_response(400, "请求体不能为空")
        
        name = data.get('name')
        if not name:
            return create_response(400, "知识库名称不能为空")
        
        description = data.get('description', f"知识库: {name}")
        indexing_technique = data.get('indexing_technique', 'high_quality')
        embedding_model = data.get('embedding_model', 'text-embedding-v3')
        embedding_model_provider = data.get('embedding_model_provider', 'tongyi')
        
        client = DifyAPIClient()
        dataset_id = client.create_knowledge_base(
            name=name,
            description=description,
            indexing_technique=indexing_technique,
            embedding_model=embedding_model,
            embedding_model_provider=embedding_model_provider
        )
        
        if dataset_id:
            # 异步调用工作流通知（不阻塞主流程）
            try:
                workflow_client.notify_knowledge_base_update(
                    action="create",
                    dataset_id=dataset_id,
                    dataset_name=name
                )
            except Exception as workflow_e:
                logger.warning(f"工作流通知失败，但不影响知识库创建: {str(workflow_e)}")
            
            return create_response(200, "知识库创建成功", {
                "dataset_id": dataset_id,
                "name": name
            })
        else:
            return create_response(500, "知识库创建失败", error="创建知识库返回空ID")
            
    except Exception as e:
        logger.error(f"创建知识库失败: {str(e)}")
        return create_response(500, "创建知识库失败", error=str(e))

@app.route('/api/datasets/<dataset_id>', methods=['GET'])
def get_dataset_detail(dataset_id: str):
    """获取知识库详情"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        client = DifyAPIClient()
        detail = client.get_dataset_detail(actual_dataset_id)
        
        if detail:
            # 添加知识库管理信息（包含工作流key）
            kb_config = settings.get_knowledge_base_config(actual_dataset_id)
            if kb_config:
                detail['knowledge_base_info'] = {
                    'workflow_key': kb_config.get('workflow_key', ''),
                    'workflow_config': {
                        'enabled': kb_config.get('workflow_config', {}).get('enabled', False),
                        'base_url': kb_config.get('workflow_config', {}).get('base_url', ''),
                        'endpoint': kb_config.get('workflow_config', {}).get('endpoint', '')
                    }
                }
            return create_response(200, "获取知识库详情成功", detail)
        else:
            return create_response(404, "知识库不存在")
            
    except Exception as e:
        logger.error(f"获取知识库详情失败: {str(e)}")
        return create_response(500, "获取知识库详情失败", error=str(e))

@app.route('/api/datasets/<dataset_id>', methods=['PATCH'])
def update_dataset(dataset_id: str):
    """更新知识库"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        data = request.get_json()
        if not data:
            return create_response(400, "请求体不能为空")
        
        client = DifyAPIClient()
        result = client.update_dataset(
            dataset_id=actual_dataset_id,
            name=data.get('name'),
            indexing_technique=data.get('indexing_technique'),
            permission=data.get('permission'),
            embedding_model_provider=data.get('embedding_model_provider'),
            embedding_model=data.get('embedding_model'),
            search_method=data.get('search_method'),
            reranking_enable=data.get('reranking_enable'),
            top_k=data.get('top_k'),
            score_threshold_enabled=data.get('score_threshold_enabled'),
            score_threshold=data.get('score_threshold')
        )
        
        if result:
            return create_response(200, "知识库更新成功", result)
        else:
            return create_response(500, "知识库更新失败")
            
    except Exception as e:
        logger.error(f"更新知识库失败: {str(e)}")
        return create_response(500, "更新知识库失败", error=str(e))

@app.route('/api/datasets/<dataset_id>', methods=['DELETE'])
def delete_dataset(dataset_id: str):
    """删除知识库"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        client = DifyAPIClient()
        result = client.delete_dataset(actual_dataset_id)
        
        if result:
            return create_response(200, "知识库删除成功")
        else:
            return create_response(500, "知识库删除失败")
            
    except Exception as e:
        logger.error(f"删除知识库失败: {str(e)}")
        return create_response(500, "删除知识库失败", error=str(e))

# 文档管理接口
@app.route('/api/datasets/<dataset_id>/documents', methods=['GET'])
def list_documents(dataset_id):
    """获取知识库文档列表"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        client = DifyAPIClient()
        documents = client.get_documents(actual_dataset_id)
        
        return create_response(200, "获取文档列表成功", {
            "documents": documents,
            "total": len(documents)
        })
    except Exception as e:
        logger.error(f"获取文档列表失败: {str(e)}")
        return create_response(500, "获取文档列表失败", error=str(e))

@app.route('/api/datasets/<dataset_id>/documents', methods=['POST'])
def create_document_by_text(dataset_id: str):
    """通过文本创建文档或通过文件下载链接创建文档"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        data = request.get_json()
        if not data:
            return create_response(400, "请求体不能为空")
        
        # 合并JSON与Form参数以提升兼容性
        merged = {}
        if isinstance(data, dict):
            merged.update(data)
        try:
            for k in request.form.keys():
                v = request.form.get(k)
                # 如果JSON中没有或值为空，用Form值覆盖
                if k not in merged or merged.get(k) in [None, "", []]:
                    merged[k] = v
        except Exception:
            pass

        name = merged.get('name')
        text = merged.get('text')
        # 兼容多种可能的URL字段名
        _url_keys = [
            'file_url', 'url', 'fileUrl', 'download_url', 'downloadUrl',
            'download_address', 'downloadAddress', 'link', 'file_link', 'fileLink'
        ]
        file_url = None
        file_url_key = None
        for k in _url_keys:
            v = merged.get(k)
            if v:
                file_url = v
                file_url_key = k
                break
        # 兼容多种可能的下载头字段名
        _hdr_keys = ['download_headers', 'headers', 'http_headers', 'downloadHeaders']
        download_headers = None
        download_headers_key = None
        for hk in _hdr_keys:
            hv = merged.get(hk)
            if isinstance(hv, dict) and hv:
                download_headers = hv
                download_headers_key = hk
                break
        # 如果下载头以字符串形式传入，尝试解析为JSON
        if (download_headers is None) and isinstance(merged.get('download_headers'), str):
            import json
            try:
                download_headers = json.loads(merged.get('download_headers'))
                download_headers_key = 'download_headers'
            except Exception:
                pass

        # 深度扫描嵌套结构中的URL与headers键（如 payload.fileUrl 或 data.headers）
        def _deep_find_keys(obj, keys, path=""):
            results = []
            try:
                if isinstance(obj, dict):
                    for dk, dv in obj.items():
                        new_path = f"{path}.{dk}" if path else dk
                        if dk in keys and dv:
                            results.append((new_path, dv))
                        results.extend(_deep_find_keys(dv, keys, new_path))
                elif isinstance(obj, list):
                    for idx, item in enumerate(obj):
                        results.extend(_deep_find_keys(item, keys, f"{path}[{idx}]"))
            except Exception:
                pass
            return results

        deep_url_hits = _deep_find_keys(merged, _url_keys)
        deep_hdr_hits = _deep_find_keys(merged, _hdr_keys)

        if not file_url and deep_url_hits:
            file_url_key, file_url = deep_url_hits[0][0], deep_url_hits[0][1]
        if not download_headers and deep_hdr_hits:
            download_headers_key, download_headers = deep_hdr_hits[0][0], deep_hdr_hits[0][1] if isinstance(deep_hdr_hits[0][1], dict) else None

        # 如果file_url仍为空，尝试从text字段的JSON字符串或纯文本中提取URL
        text_json_hits = []
        regex_url_hit = None
        try:
            txt_field = merged.get('text')
            if isinstance(txt_field, str):
                import json as _json
                try:
                    parsed_text = _json.loads(txt_field)
                    # 从解析后的结构中深度查找可能的URL键
                    _text_hits = _deep_find_keys(parsed_text, _url_keys)
                    text_json_hits = [p for p, _ in _text_hits]
                    if (not file_url) and _text_hits:
                        file_url_key = f"text.{_text_hits[0][0]}"
                        file_url = _text_hits[0][1]
                except Exception:
                    pass
                # 如果仍未找到，使用正则从纯文本中提取第一个URL
                if not file_url:
                    import re as _re
                    m = _re.search(r'https?://[^\s"\'<>]+', txt_field)
                    if m:
                        regex_url_hit = m.group(0)
                        file_url = regex_url_hit
                        file_url_key = 'text_regex'
        except Exception:
            pass

        # 详细记录请求原始体与解析后的键，帮助定位为何 file_url 为空
        try:
            raw_body = request.get_data(as_text=True)
            content_type = request.headers.get('Content-Type', '')
            args_dict = request.args.to_dict()
            form_keys = list(request.form.keys())
            file_keys = list(request.files.keys()) if request.files else []
            body_preview = (raw_body[:800] if raw_body else '')
            import json as _json
            merged_keys = list(merged.keys()) if isinstance(merged, dict) else []
            merged_preview = ''
            try:
                merged_preview = _json.dumps(merged, ensure_ascii=False)[:800] if isinstance(merged, dict) else ''
            except Exception:
                merged_preview = str(merged)[:800]
            logger.info(
                f"请求信息: content_type={content_type}, args={args_dict}, form_keys={form_keys}, files_keys={file_keys}, body_len={len(raw_body or '')}, body_preview={body_preview}"
            )
            logger.info(
                f"解析后JSON: keys={merged_keys}, json_preview={merged_preview}"
            )
        except Exception:
            pass

        # 记录输入参数，确认是否收到下载链接与鉴权头
        try:
            headers_keys = list((download_headers or {}).keys())
            present_url_keys_json = [k for k in _url_keys if isinstance(data, dict) and k in data]
            present_url_keys_form = [k for k in _url_keys if k in request.form]
            present_hdr_keys_json = [k for k in _hdr_keys if isinstance(data, dict) and k in data]
            present_hdr_keys_form = [k for k in _hdr_keys if k in request.form]
            logger.info(
                f"收到URL/文本上传: dataset={dataset_id} -> {actual_dataset_id}, "
                f"name={name}, has_text={bool(text)}, file_url={file_url}, file_url_key={file_url_key}, "
                f"present_url_keys_json={present_url_keys_json}, present_url_keys_form={present_url_keys_form}, deep_url_hits={[p for p,_ in deep_url_hits]}, text_json_hits={text_json_hits}, text_regex_hit={regex_url_hit}, "
                f"headers_keys={headers_keys}, headers_key={download_headers_key}, present_hdr_keys_json={present_hdr_keys_json}, present_hdr_keys_form={present_hdr_keys_form}, deep_hdr_hits={[p for p,_ in deep_hdr_hits]}"
            )
        except Exception:
            pass

        # 如果打开调试开关，则在响应中附带详细入参回显
        def _collect_debug_data():
            try:
                raw_body = request.get_data(as_text=True)
                args_dict = request.args.to_dict()
                form_items = {k: request.form.get(k) for k in request.form.keys()}
                files_info = {k: v.filename for k, v in request.files.items()} if request.files else {}
                return {
                    "content_type": request.headers.get('Content-Type', ''),
                    "args": args_dict,
                    "form": form_items,
                    "files": files_info,
                    "json": merged,
                    "raw_body_preview": (raw_body[:800] if raw_body else ''),
                    "url_detection": {
                        "selected_key": file_url_key,
                        "selected_value": file_url,
                        "present_json_keys": present_url_keys_json,
                        "present_form_keys": present_url_keys_form,
                        "deep_hits": [p for p,_ in deep_url_hits],
                        "text_json_hits": text_json_hits,
                        "text_regex_hit": regex_url_hit
                    },
                    "headers_detection": {
                        "selected_key": download_headers_key,
                        "present_json_keys": present_hdr_keys_json,
                        "present_form_keys": present_hdr_keys_form,
                        "deep_hits": [p for p,_ in deep_hdr_hits],
                        "keys": list((download_headers or {}).keys())
                    }
                }
            except Exception:
                return {}

        debug_enabled = str(request.args.get('debug', '')).lower() in ['1', 'true', 'yes']

        # 构建从 text 中解析出的URL列表（批量支持）
        urls_entries = []
        try:
            if isinstance(text, str):
                import json as _json
                parsed_text = None
                try:
                    parsed_text = _json.loads(text)
                except Exception:
                    parsed_text = None
                if isinstance(parsed_text, list):
                    for idx, item in enumerate(parsed_text):
                        if isinstance(item, dict):
                            # 逐项识别可能的URL键
                            for k in _url_keys:
                                v = item.get(k)
                                if v:
                                    urls_entries.append({
                                        "url": v,
                                        "name": item.get('name') or '',
                                        "source_key": f"text.[{idx}].{k}"
                                    })
                                    break
        except Exception:
            pass

        # 验证参数（若既无文本也无任何URL则报错）
        if not text and not file_url and not urls_entries:
            return create_response(400, "文本内容或文件下载链接必须提供其中一个")

        client = DifyAPIClient()
        
        # 如果有文件下载链接或从 text 中解析出多个链接，优先处理文件下载
        if file_url or urls_entries:
            try:
                # 批量模式：若存在从 text 解析出的多个链接，则逐一处理
                if urls_entries and len(urls_entries) > 1:
                    logger.info(f"批量处理URL: count={len(urls_entries)}")
                    results = []
                    failures = []
                    for i, entry in enumerate(urls_entries, start=1):
                        _url = entry.get('url')
                        _item_name = entry.get('name') or ''
                        try:
                            hdr_keys = list((download_headers or {}).keys())
                            logger.info(f"[{i}/{len(urls_entries)}] 准备下载文件: file_url={_url}, headers_keys={hdr_keys}")
                        except Exception:
                            pass
                        try:
                            downloaded_file_path = client.download_file_from_url(_url, headers=download_headers)
                            if not downloaded_file_path:
                                failures.append({"url": _url, "error": "文件下载失败"})
                                continue
                            # 重命名：优先使用条目名称，其次使用URL基名；保留扩展名
                            upload_file_path = downloaded_file_path
                            try:
                                base, ext = os.path.splitext(downloaded_file_path)
                                if not ext:
                                    ext = ''
                                # 条目名或URL基名作为文档名
                                final_name = _item_name.strip()
                                if not final_name:
                                    try:
                                        from urllib.parse import urlsplit
                                        import ntpath
                                        final_name = ntpath.basename(urlsplit(_url).path) or f"document_{i}"
                                        # 如果URL基名包含扩展名，移除以便统一加上ext
                                        fn_base, fn_ext = os.path.splitext(final_name)
                                        final_name = fn_base
                                        if not ext and fn_ext:
                                            ext = fn_ext
                                    except Exception:
                                        final_name = f"document_{i}"
                                safe_name = secure_filename(final_name)
                                if safe_name:
                                    safe_name = f"{safe_name}{ext}"
                                    new_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_name)
                                    try:
                                        os.replace(downloaded_file_path, new_path)
                                        upload_file_path = new_path
                                    except Exception:
                                        upload_file_path = downloaded_file_path
                            except Exception:
                                upload_file_path = downloaded_file_path

                            result = client.upload_document(upload_file_path, actual_dataset_id)
                            if result and result.get('document'):
                                results.append({
                                    "document": result['document'],
                                    "source_url": _url,
                                    "name": result['document'].get('name', _item_name or ''),
                                })
                                try:
                                    logger.success(f"[{i}/{len(urls_entries)}] 文档上传成功: {result['document'].get('name', '')}")
                                except Exception:
                                    pass
                            else:
                                failures.append({"url": _url, "error": "上传失败"})
                        except Exception as e:
                            failures.append({"url": _url, "error": str(e)})

                    # 获取对应的app key
                    app_key = None
                    for key, value in simple_id_to_uuid.items():
                        if value == actual_dataset_id:
                            app_key = get_app_key_by_number(key)
                            break

                    resp_payload = {
                        "success": len(results),
                        "total": len(urls_entries),
                        "dataset_id": actual_dataset_id,
                        "documents": results,
                        "failed": failures,
                        "app_key": app_key
                    }
                    if debug_enabled:
                        resp_payload["debug"] = _collect_debug_data()
                    return create_response(200, "批量文件下载并创建文档成功", resp_payload)

                # 单文件模式：处理单个 file_url（可能来自顶层或 text 中的第一个命中）
                try:
                    headers_keys = list((download_headers or {}).keys())
                    logger.info(f"准备下载文件: file_url={file_url}, headers_keys={headers_keys}")
                except Exception:
                    pass
                # 先下载文件到本地
                downloaded_file_path = client.download_file_from_url(file_url, headers=download_headers)
                if not downloaded_file_path:
                    return create_response(500, "文件下载失败")
                
                # 如果提供了自定义名称，则以该名称重命名上传（避免Dify按同名覆盖/去重）
                upload_file_path = downloaded_file_path
                try:
                    if name:
                        base, ext = os.path.splitext(downloaded_file_path)
                        if not ext:
                            ext = ''
                        safe_name = name
                        # 简单清理非法路径字符
                        for ch in ['\\', '/', ':', '*', '?', '"', '<', '>', '|']:
                            safe_name = safe_name.replace(ch, '_')
                        renamed_path = os.path.join(os.path.dirname(downloaded_file_path), f"{safe_name}{ext}")
                        if renamed_path != downloaded_file_path:
                            try:
                                # 复制为新文件名，避免移动失败导致清理困难
                                with open(downloaded_file_path, 'rb') as src, open(renamed_path, 'wb') as dst:
                                    dst.write(src.read())
                                upload_file_path = renamed_path
                            except Exception as rename_e:
                                logger.warning(f"重命名上传文件失败，继续使用原始文件名: {str(rename_e)}")
                except Exception as e:
                    logger.warning(f"处理自定义文件名失败，继续使用原始文件名: {str(e)}")

                # 上传下载的文件
                result = client.upload_document(upload_file_path, actual_dataset_id)
                
                # 删除临时下载的文件
                try:
                    os.remove(downloaded_file_path)
                    if upload_file_path and upload_file_path != downloaded_file_path:
                        try:
                            os.remove(upload_file_path)
                        except Exception as remove_renamed_e:
                            logger.warning(f"无法删除重命名临时文件 {upload_file_path}: {str(remove_renamed_e)}")
                except Exception as remove_e:
                    logger.warning(f"无法删除临时文件 {downloaded_file_path}: {str(remove_e)}")
                
                if result and result.get('document'):
                    # 异步调用工作流通知
                    try:
                        dataset_info = client.get_dataset_detail(actual_dataset_id)
                        dataset_name = dataset_info.get('name', '未知知识库') if dataset_info else '未知知识库'
                        
                        # 获取工作流key
                        kb_config = settings.get_knowledge_base_config(actual_dataset_id)
                        workflow_key = kb_config.get('workflow_key', '') if kb_config else ''
                        
                        document_info = result.get('document', {})
                        doc_name = document_info.get('name', name)
                        # 从API响应中提取文件信息
                        file_info = {
                            "filename": doc_name,
                            "size": document_info.get('data_source', {}).get('info', {}).get('size', 0),
                            "source_url": f"/files/{actual_dataset_id}/{document_info.get('id', '')}"
                        }
                        
                        workflow_client.notify_document_update(
                            dataset_id=actual_dataset_id,
                            document_id=document_info.get('id', ''),
                            event_type="upload",
                            document_info={
                                "name": doc_name,
                                "type": "file",
                                "workflow_key": workflow_key,
                                "file_info": file_info
                            }
                        )
                    except Exception as workflow_e:
                        logger.warning(f"工作流通知失败，但不影响文档创建: {str(workflow_e)}")
                    
                    # 获取对应的app key
                    app_key = None
                    for key, value in simple_id_to_uuid.items():
                        if value == actual_dataset_id:
                            app_key = get_app_key_by_number(key)
                            break
                        
                    # 日志：记录URL上传请求的关键入参，便于核对是否传入正确下载地址
                    try:
                        _name = locals().get('doc_name') or locals().get('name') or ''
                        _hdr_keys = list(download_headers.keys()) if 'download_headers' in locals() and isinstance(download_headers, dict) else []
                        logger.info(f"收到URL上传: dataset={dataset_id} -> {actual_dataset_id}, name={_name}, file_url={file_url}, headers_keys={_hdr_keys}")
                    except Exception:
                        pass

                    resp_payload = {
                        "document": result['document'],
                        "document_id": result['document'].get('id', ''),
                        "dataset_id": actual_dataset_id,
                        "source_url": file_url,
                        "app_key": app_key
                    }
                    if debug_enabled:
                        resp_payload["debug"] = _collect_debug_data()
                    return create_response(200, "文件下载并创建文档成功", resp_payload)
                else:
                    return create_response(500, "文件上传失败", error="创建文档返回空结果")
                    
            except Exception as e:
                logger.error(f"处理文件下载链接失败: {str(e)}")
                return create_response(500, "处理文件下载链接失败", error=str(e))
        
        # 原有的文本创建逻辑（仅在未提供任何URL时执行）
        if not name:
            return create_response(400, "文档名称不能为空")
        indexing_technique = data.get('indexing_technique', 'high_quality')
        doc_form = data.get('doc_form', 'text_model')
        doc_language = data.get('doc_language', 'Chinese')
        
        result = client.create_document_by_text(
            dataset_id=actual_dataset_id,
            name=name,
            text=text,
            indexing_technique=indexing_technique,
            doc_form=doc_form,
            doc_language=doc_language
        )
        
        if result and result.get('document'):
            # 异步调用工作流通知（不阻塞主流程）
            try:
                # 获取知识库信息用于通知
                dataset_info = client.get_dataset_detail(actual_dataset_id)
                dataset_name = dataset_info.get('name', '未知知识库') if dataset_info else '未知知识库'
                
                document_info = result.get('document', {})
                notify_doc_info = {
                    "name": document_info.get('name', name),
                    "type": "text",
                    "dataset_name": dataset_name,
                    "size": len(text),
                    "word_count": 0,
                    "indexing_status": document_info.get('indexing_status', 'parsing')
                }
                workflow_client.notify_document_update(
                    dataset_id=actual_dataset_id,
                    document_id=document_info.get('id', ''),
                    event_type="create",
                    document_info=notify_doc_info
                )
            except Exception as workflow_e:
                logger.warning(f"工作流通知失败，但不影响文档创建: {str(workflow_e)}")
            
            # 获取对应的app key
            app_key = None
            for key, value in simple_id_to_uuid.items():
                if value == actual_dataset_id:
                    app_key = get_app_key_by_number(key)
                    break

            resp_payload = {
                "document": result['document'],
                "app_key": app_key
            }
            if debug_enabled:
                resp_payload["debug"] = _collect_debug_data()
            return create_response(200, "文档创建成功", resp_payload)
        else:
            return create_response(500, "文档创建失败", error="创建文档返回空结果")
            
    except Exception as e:
        logger.error(f"创建文档失败: {str(e)}")
        return create_response(500, "创建文档失败", error=str(e))

@app.route('/api/datasets/<dataset_id>/documents/upload', methods=['POST'])
def upload_document(dataset_id: str):
    """上传文件到知识库"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        if 'file' not in request.files:
            return create_response(400, "没有上传文件")
        
        file = request.files['file']
        if file.filename == '':
            return create_response(400, "文件名为空")
        
        # 保存上传的文件
        filename = secure_filename(file.filename)
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(file_path)
        # 日志：记录文件直传接口的文件名与数据集映射，便于排查
        try:
            logger.info(f"收到文件直传: dataset={dataset_id} -> {actual_dataset_id}, filename={filename}, path={file_path}")
        except Exception:
            pass
        
        client = DifyAPIClient()
        result = client.upload_document(file_path, actual_dataset_id)
        
        # 清理临时文件
        os.remove(file_path)
        
        if result:
            # 异步调用工作流通知（不阻塞主流程）
            try:
                # 获取知识库信息用于通知
                dataset_info = client.get_dataset_detail(actual_dataset_id)
                dataset_name = dataset_info.get('name', '未知知识库') if dataset_info else '未知知识库'
                
                # 获取工作流key
                kb_config = settings.get_knowledge_base_config(actual_dataset_id)
                workflow_key = kb_config.get('workflow_key', '') if kb_config else ''
                
                document_info = result.get('document', {})
                workflow_client.notify_document_update(
                    event_type="upload",
                    dataset_id=actual_dataset_id,
                    dataset_name=dataset_name,
                    document_id=document_info.get('id', ''),
                    document_name=document_info.get('name', filename),
                    document_type="file",
                    workflow_key=workflow_key,
                    file_info={
                        "filename": filename,
                        "size": os.path.getsize(file_path) if os.path.exists(file_path) else 0
                    }
                )
            except Exception as workflow_e:
                logger.warning(f"工作流通知失败，但不影响文件上传: {str(workflow_e)}")
            
            # 获取工作流key用于响应
            kb_config = settings.get_knowledge_base_config(actual_dataset_id)
            workflow_key = kb_config.get('workflow_key', '') if kb_config else ''
            
            # 获取对应的app key
            app_key = None
            for key, value in simple_id_to_uuid.items():
                if value == actual_dataset_id:
                    app_key = get_app_key_by_number(key)
                    break
            
            return create_response(200, "文件上传成功", {
                "document": result.get('document', {}),
                "workflow_key": workflow_key,
                "app_key": app_key
            })
        else:
            return create_response(500, "文件上传失败")
            
    except Exception as e:
        logger.error(f"上传文件失败: {str(e)}")
        return create_response(500, "上传文件失败", error=str(e))

@app.route('/api/datasets/<dataset_id>/documents/<document_id>', methods=['DELETE'])
def delete_document(dataset_id: str, document_id: str):
    """删除文档"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        client = DifyAPIClient()
        result = client.delete_document(actual_dataset_id, document_id)
        
        if result:
            return create_response(200, "文档删除成功")
        else:
            return create_response(500, "文档删除失败")
            
    except Exception as e:
        logger.error(f"删除文档失败: {str(e)}")
        return create_response(500, "删除文档失败", error=str(e))

@app.route('/api/datasets/<dataset_id>/documents/<document_id>', methods=['PATCH'])
def update_document_by_file(dataset_id: str, document_id: str):
    """通过文件更新文档"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        if 'file' not in request.files:
            return create_response(400, "没有上传文件")
        
        file = request.files['file']
        if file.filename == '':
            return create_response(400, "文件名为空")
        
        name = request.form.get('name')
        
        # 保存上传的文件
        filename = secure_filename(file.filename)
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(file_path)
        
        client = DifyAPIClient()
        result = client.update_document_by_file(actual_dataset_id, document_id, file_path, name)
        
        # 清理临时文件
        os.remove(file_path)
        
        if result:
            # 获取工作流key用于响应
            kb_config = settings.get_knowledge_base_config(actual_dataset_id)
            workflow_key = kb_config.get('workflow_key', '') if kb_config else ''
            
            # 获取对应的app key
            app_key = None
            for key, value in simple_id_to_uuid.items():
                if value == actual_dataset_id:
                    app_key = get_app_key_by_number(key)
                    break
            
            return create_response(200, "文档更新成功", {
                "document": result.get('document', {}),
                "workflow_key": workflow_key,
                "app_key": app_key
            })
        else:
            return create_response(500, "文档更新失败")
            
    except Exception as e:
        logger.error(f"更新文档失败: {str(e)}")
        return create_response(500, "更新文档失败", error=str(e))

@app.route('/api/datasets/<dataset_id>/documents/<document_id>/status', methods=['GET'])
def get_document_status(dataset_id: str, document_id: str):
    """获取文档状态"""
    try:
        # 检查权限
        permission_check = require_dataset_permission(dataset_id)
        if permission_check:
            return permission_check
        
        # Convert simple ID to UUID if applicable
        actual_dataset_id = dataset_id
        if dataset_id.isdigit() and dataset_id in simple_id_to_uuid:
            actual_dataset_id = simple_id_to_uuid[dataset_id]
        
        client = DifyAPIClient()
        result = client.get_document_status(actual_dataset_id, document_id)
        
        if result and result.get('document'):
            return create_response(200, "获取文档状态成功", {
                "document": result['document']
            })
        else:
            return create_response(404, "文档不存在")
            
    except Exception as e:
        logger.error(f"获取文档状态失败: {str(e)}")
        return create_response(500, "获取文档状态失败", error=str(e))

# 安全中间件
def require_api_key(f):
    """API Key验证装饰器"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not settings.api_key_required:
            return f(*args, **kwargs)
        
        # 检查API Key
        api_key = request.headers.get('X-API-Key') or request.headers.get('Authorization', '').replace('Bearer ', '')
        
        if not api_key:
            logger.warning(f"未提供API Key - IP: {request.remote_addr}, Endpoint: {request.endpoint}")
            return create_response(401, "未提供API Key", error="Missing API Key")
        
        if settings.api_key and api_key != settings.api_key:
            logger.warning(f"无效API Key - IP: {request.remote_addr}, Key: {api_key[:10]}..., Endpoint: {request.endpoint}")
            return create_response(401, "无效的API Key", error="Invalid API Key")
        
        return f(*args, **kwargs)
    return decorated_function

def is_internal_ip(ip_str: str) -> bool:
    """检查是否为内网IP"""
    try:
        ip = ipaddress.ip_address(ip_str)
        return ip.is_private or ip.is_loopback
    except ValueError:
        return False

@app.before_request
def log_external_access():
    """记录外网访问日志"""
    if settings.log_external_access and not is_internal_ip(request.remote_addr):
        logger.info(f"🌐 外网访问 - IP: {request.remote_addr}, Method: {request.method}, Path: {request.path}, User-Agent: {request.headers.get('User-Agent', 'Unknown')}")

@app.before_request
def security_headers():
    """设置安全响应头"""
    g.start_time = request.environ.get('REQUEST_START_TIME')

@app.after_request
def add_security_headers(response):
    """添加安全响应头"""
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['X-XSS-Protection'] = '1; mode=block'
    response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
    return response

def main():
    """启动Flask应用"""
    # 配置日志
    logger.remove()
    logger.add(
        "logs/api.log",
        rotation="10 MB",
        retention="7 days",
        level=settings.log_level,
        format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
    )
    logger.add(
        "logs/api_error.log",
        rotation="10 MB",
        retention="7 days",
        level="ERROR",
        format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
    )
    
    # 创建日志目录
    os.makedirs("logs", exist_ok=True)
    
    logger.info("🚀 启动Dify知识库管理API服务")
    logger.info(f"📋 服务配置:")
    logger.info(f"   Dify服务器: {settings.dify_base_url}")
    logger.info(f"   日志级别: {settings.log_level}")
    logger.info(f"   临时上传目录: {UPLOAD_FOLDER}")
    
    # 启动应用
    app.run(host='0.0.0.0', port=5000, debug=True)

if __name__ == '__main__':
    main()